#!/usr/bin/env bash
set -CeE
set -o pipefail
if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then
  cat << EOF >&2
WARNING: bash ${BASH_VERSION} does not support several modern safety features.
This script was written with the latest POSIX standard in mind, and was only
tested with modern shell standards. This script may not perform correctly in
this environment.
EOF
  sleep 1
else
  set -u
fi

### These are hooks for Cloud Build to be able to use debug/staging images
### when necessary. Don't set these environment variables unless you're testing
### in CI/CD.
_CI_ASM_IMAGE_LOCATION="${_CI_ASM_IMAGE_LOCATION:=}"
_CI_ASM_IMAGE_TAG="${_CI_ASM_IMAGE_TAG:=}"
_CI_ASM_PKG_LOCATION="${_CI_ASM_PKG_LOCATION:=}"
_CI_ISTIOCTL_REL_PATH="${_CI_ISTIOCTL_REL_PATH:=}"
_CI_REVISION_PREFIX="${_CI_REVISION_PREFIX:=}"

### Internal variables ###
MAJOR="${MAJOR:=1}"; readonly MAJOR;
MINOR="${MINOR:=10}"; readonly MINOR;
POINT="${POINT:=3}"; readonly POINT;
REV="${REV:=12}"; readonly REV;
CONFIG_VER="${CONFIG_VER:="1-unstable"}"; readonly CONFIG_VER;
ASM_RELEASE="${MAJOR}.${MINOR}"
ASM_META_VERSION="${ASM_RELEASE}.0"
ASM_FULL_VERSION="${MAJOR}.${MINOR}.${POINT}"
readonly ASM_RELEASE
readonly ASM_META_VERSION
readonly ASM_FULL_VERSION
RELEASE=""

SERVICE_PROXY_AGENT_BUCKET="${SERVICE_PROXY_AGENT_BUCKET:=}"
ASM_REVISION_LABEL_KEY="istio.io/rev"
readonly ASM_REVISION_LABEL_KEY
EXPANSION_GATEWAY_NAME="istio-eastwestgateway"
readonly EXPANSION_GATEWAY_NAME
ISTIOD_NAME="istiod"
readonly ISTIOD_NAME
IDENTITY_PROVIDER_ANNOTATION_KEY="security.cloud.google.com/IdentityProvider"
readonly IDENTITY_PROVIDER_ANNOTATION_KEY
CROSS_CLUSTER_WORKLOADENTRY_KEY="PILOT_ENABLE_CROSS_CLUSTER_WORKLOAD_ENTRY"
readonly CROSS_CLUSTER_WORKLOADENTRY_KEY

SCRIPT_NAME="${0##*/}"
APATH=""
AKUBECTL=""
AKPT=""
AGCLOUD=""
ISTIO_FOLDER_NAME=""
ISTIOCTL_REL_PATH=""
PACKAGE_DIRECTORY=""
KPT_BRANCH=""
KPT_URL=""
REVISION_LABEL=""
EXPOSE_ISTIOD_SERVICE=""
EXPANSION_GATEWAY_FILE=""

PROJECT_NUMBER=""

### Cluster variables ###
ROOT_CERT=""
INGRESS_IP=""
NETWORK=""
MEMBERSHIP_UID=""
GKE_URL=""
ENVIRON_PROJECT_ID=""
CLUSTER_ID=""
declare -A ASM_REVISIONS
MESH_CONFIG_NAME=""

### Workload variables ###
WORKLOAD_LABELS=""
WORKLOAD_SERVICE_ACCOUNT=""
WORKLOAD_ASM_REVISION=""
WORKLOAD_IDENTITY_PROVIDER=""
CREDENTIAL_IDENTITY_PROVIDER=""
CANONICAL_SERVICE=""
CANONICAL_REVISION=""

### Command internal toggle ###
CREATE_GCE_INSTANCE_TEMPLATE=0
PREPARE_CLUSTER=0

INSTALL_EXPANSION_GATEWAY=0
INSTALL_IDENTITY_PROVIDER=0

### Option variables ###
PROJECT_ID="${PROJECT_ID:=}"
CLUSTER_NAME="${CLUSTER_NAME:=}"
CLUSTER_LOCATION="${CLUSTER_LOCATION:=}"
SOURCE_INSTANCE_TEMPLATE="${SOURCE_INSTANCE_TEMPLATE:=}"
WORKLOAD_NAME="${WORKLOAD_NAME:=}"
WORKLOAD_NAMESPACE="${WORKLOAD_NAMESPACE:=}"

SERVICE_ACCOUNT="${SERVICE_ACCOUNT:=}"
KEY_FILE="${KEY_FILE:=}"

DRY_RUN="${DRY_RUN:=0}"
ONLY_VALIDATE="${ONLY_VALIDATE:=0}"
VERBOSE="${VERBOSE:=0}"

SOURCE_IMAGE_PATH=""
SUPPORTED_VM_DISTROS=("debian-9" "debian-10" "centos-8" "centos-7")

init() {
  # BSD-style readlink apparently doesn't have the same -f toggle on readlink
  case "$(uname)" in
    Linux ) APATH="readlink";;
    Darwin) APATH="stat";;
    *);;
  esac

  if [[ "${POINT}" == "alpha" ]]; then
    RELEASE="${MAJOR}.${MINOR}-alpha.${REV}"
    KPT_BRANCH="${_CI_ASM_KPT_BRANCH:=master}"
    REVISION_LABEL="${_CI_REVISION_PREFIX}asm-${MAJOR}${MINOR}${POINT}"
  else
    RELEASE="${MAJOR}.${MINOR}.${POINT}-asm.${REV}"
    KPT_BRANCH="${_CI_ASM_KPT_BRANCH:=release-${MAJOR}.${MINOR}-asm}"
    REVISION_LABEL="${_CI_REVISION_PREFIX}asm-${MAJOR}${MINOR}${POINT}-${REV}"
  fi
  readonly RELEASE; readonly KPT_BRANCH; readonly REVISION_LABEL;

  PACKAGE_DIRECTORY="asm/istio"; readonly PACKAGE_DIRECTORY;
  ISTIO_FOLDER_NAME="istio-${RELEASE}"; readonly ISTIO_FOLDER_NAME;
  ISTIOCTL_REL_PATH="${ISTIO_FOLDER_NAME}/bin/istioctl"; readonly ISTIOCTL_REL_PATH;
  EXPOSE_ISTIOD_SERVICE="${PACKAGE_DIRECTORY}/expansion/expose-istiod.yaml"; readonly EXPOSE_ISTIOD_SERVICE;
  EXPANSION_GATEWAY_FILE="${PACKAGE_DIRECTORY}/expansion/vm-eastwest-gateway.yaml"; readonly EXPANSION_GATEWAY_FILE;

  AKUBECTL="$(which kubectl || true)"
  AGCLOUD="$(which gcloud || true)"
  AKPT="$(which kpt || true)"
}

apath() {
  "${APATH}" "${@}"
}

gcloud() {
  run "${AGCLOUD}" "${@}"
}

kubectl() {
  run "${AKUBECTL}" "${@}"
}

kpt() {
  run "${AKPT}" "${@}"
}

istioctl() {
  run "$(istioctl_path)" "${@}"
}

istioctl_path() {
  if [[ -n "${_CI_ISTIOCTL_REL_PATH}" && -f "${_CI_ISTIOCTL_REL_PATH}" ]]; then
    echo "${_CI_ISTIOCTL_REL_PATH}"
  else
    echo "./${ISTIOCTL_REL_PATH}"
  fi
}

### Convenience functions ###

#######
# run takes a list of arguments that represents a command
# If DRY_RUN or VERBOSE is enabled, it will print the command, and if DRY_RUN is
# not enabled it runs the command.
#######
run() {
  if [[ "${DRY_RUN}" -eq 1 ]]; then
    warn "Would have executed: ${*}"
    return
  elif [[ "${VERBOSE}" -eq 0 ]]; then
    "${@}" 2>/dev/null
    return "$?"
  fi
  info "Running: '${*}'"
  info "-------------"
  local RETVAL
  { "${@}"; RETVAL="$?"; } || true
  return $RETVAL
}

#######
# retry takes an integer N as the first argument, and a list of arguments
# representing a command afterwards. It will retry the given command up to N
# times before returning 1. If the command is kubectl, it will try to
# re-get credentials in case something caused the k8s IP to change.
#######
retry() {
  local MAX_TRIES; MAX_TRIES="${1}";
  shift 1
  for i in $(seq 0 "${MAX_TRIES}"); do
    if [[ "${i}" -eq "${MAX_TRIES}" ]]; then
      false
      return
    fi
    { "${@}" && return 0; } || true
    warn "Failed, retrying...($((i+1)) of ${MAX_TRIES})"
    sleep 2
    if [[ "$1" == "kubectl" ]]; then
      configure_kubectl
    fi
  done
  false
}

#######
# watch_for_result takes an integer N as the first argument, a regex pattern as
# the second argument (use `.*` for any value), and a list of arguments
# representing a command afterwards. It will retry the given command up to N
# seconds for output. If the output does not match the regex pattern before
# timeout, it returns 1.
######
watch_for_result() {
  local TIMEOUT; TIMEOUT="${1}";
  local PATTERN; PATTERN="${2}";
  shift 2
  if [[ "${DRY_RUN}" -ne 0 ]]; then
    warn "Would have executed: ${*}"
    return
  fi
  local VALUE; VALUE="";
  for i in $(seq 0 "${TIMEOUT}"); do
    if [[ "${i}" -eq "${TIMEOUT}" ]]; then
      warn "Timed out when waiting for the output of: ${*}"
      break
    fi
    VALUE=$("${@}") || true
    { [[ -n "${VALUE}" && "${VALUE}" =~ $PATTERN ]] \
      && echo "${VALUE}" && return 0; } || true
    sleep 1
  done
  return 1
}

strip_last_char() {
  echo "${1:0:$((${#1} - 1))}"
}

configure_kubectl(){
  info "Fetching/writing GCP credentials to kubeconfig file..."
  retry 2 run gcloud container clusters get-credentials "${CLUSTER_NAME}" \
    --project="${PROJECT_ID}" \
    --zone="${CLUSTER_LOCATION}"

  info "Verifying connectivity (20s)..."
  local RETVAL; RETVAL=0;
  kubectl cluster-info --request-timeout='20s' 1>/dev/null 2>/dev/null || RETVAL=$?
  if [[ "${RETVAL}" -ne 0 ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Couldn't connect to ${CLUSTER_NAME}.
If this is a private cluster, verify that the correct firewall rules are applied.
https://cloud.google.com/service-mesh/docs/gke-install-overview#requirements
EOF
  fi
  info "kubeconfig set to ${PROJECT_ID}/${CLUSTER_LOCATION}/${CLUSTER_NAME}..."
}

warn() {
  info "[WARNING]: ${1}" >&2
}

error() {
  info "[ERROR]: ${1}" >&2
}

info() {
  echo "${SCRIPT_NAME}: ${1}" >&2
}

fatal() {
  error "${1}"
  exit 2
}

fatal_with_usage() {
  warn "${1}"
  if [[ "${CREATE_GCE_INSTANCE_TEMPLATE}" -eq 1 ]]; then
    usage_create_gce_instance_template >&2
  elif [[ "${PREPARE_CLUSTER}" -eq 1 ]]; then
    usage_prepare_cluster >&2
  else
    usage_asm_vm >&2
  fi
  exit 2
}

version_message() {
  local VER; VER="${MAJOR}.${MINOR}.${POINT}-asm.${REV}+config${CONFIG_VER}";
  echo "${VER}"
}

usage_asm_vm() {
  cat << EOF
usage: ${SCRIPT_NAME} <COMMAND>

ASM VM command line utility to add GCE instances to Anthos Service Mesh.

COMMANDS:
  create_gce_instance_template   Create a new GCE instance template for ASM VMs.
  prepare_cluster                Set up and validate the existing ASM
                                 installation in the cluster to allow VM
                                 workloads to be added.

OPTIONS:
  -h|--help                      Show this message and exit.
  --version                      Print the version of this tool and exit.

Use "${SCRIPT_NAME} <COMMAND> --help" for more information about a command.
EOF
}

usage_create_gce_instance_template() {
  cat << EOF
usage: ${SCRIPT_NAME} create_gce_instance_template NAME [OPTION]...

Prepare a GCE VM template used for VMs to be added to an existing ASM cluster.
All options can also be passed via environment variables by using the ALL_CAPS
name. Options specified via flags take precedence over environment variables.

POSITIONAL ARGUMENTS:
  NAME                                                Name of the instance
                                                      template to create.

OPTIONS:
  -l|--cluster_location         <LOCATION>            The GCP location of the
                                                      target cluster.
  -n|--cluster_name             <NAME>                The name of the target
                                                      cluster.
  -p|--project_id               <ID>                  The GCP project ID.
  -w|--workload_name            <WORKLOAD_NAME>       The name of the
                                                      WorkloadGroup for GCE VM
                                                      workloads.
  --workload_namespace          <WORKLOAD_NAMESPACE>  (optional) The namespace
                                                      of the WorkloadGroup for
                                                      GCE VM workloads. Default
                                                      to the "default" namespace
                                                      if not specified.
  -i|--source_instance_template <TEMPLATE>            (optional) The name of the
                                                      source GCE instance
                                                      template.
  -s|--service_account          <SERVICE_ACCOUNT>     (optional) The name of a
                                                      service account used to
                                                      run the script.
  -k|--key_file                 <FILE PATH>           (optional) The key file
                                                      for a service account.
                                                      Required if
                                                      --service_account is
                                                      passed.

FLAGS:

  -v|--verbose                                        Print commands before and
                                                      after execution.
     --dry_run                                        Print commands, but don't
                                                      execute them.
     --only_validate                                  Run validation but don't
                                                      install.
  -h|--help                                           Show this message and
                                                      exit.

EXAMPLE:
The following invocation will prepare a GCE VM template named "my_vm_template"
in project "my_project" based on the WorkloadGroup "my_workload" in namespace
"foo" in ASM cluster "my_cluster" in zone "us-central1-c":
  $> ${SCRIPT_NAME} create_gce_instance_template my_vm_template \\
      -n my_cluster \\
      -p my_project \\
      -l us-central1-c \\
      -i base_vm_template \\
      -w my_workload \\
      --workload_namespace foo
EOF
}

usage_prepare_cluster() {
  cat << EOF
usage: ${SCRIPT_NAME} prepare_cluster [OPTION]...

Set up and validate the current ASM installation in the cluster to allow VM
workloads to be added. All options can also be passed via environment variables
by using the ALL_CAPS name. Options specified via flags take precedence over
environment variables.

OPTIONS:
  -l|--cluster_location         <LOCATION>            The GCP location of the
                                                      target cluster.
  -n|--cluster_name             <NAME>                The name of the target
                                                      cluster.
  -p|--project_id               <ID>                  The GCP project ID.
  -s|--service_account          <SERVICE_ACCOUNT>     (optional) The name of a
                                                      service account used to
                                                      run the script.
  -k|--key_file                 <FILE PATH>           (optional) The key file
                                                      for a service account.
                                                      Required if
                                                      --service_account is
                                                      passed.

FLAGS:

  -v|--verbose                                        Print commands before and
                                                      after execution.
     --dry_run                                        Print commands, but don't
                                                      execute them.
     --only_validate                                  Run validation but don't
                                                      install.
  -h|--help                                           Show this message and
                                                      exit.

EXAMPLE:
The following invocation will prepare the ASM cluster "my_cluster" in zone
"us-central1-c" in project "my_project" to allow VM workloads to be added:
  $> ${SCRIPT_NAME} prepare_cluster \\
      -n my_cluster \\
      -p my_project \\
      -l us-central1-c
EOF
}

arg_required() {
  if [[ ! "${2:-}" || "${2:0:1}" = '-' ]]; then
    fatal "Option ${1} requires an argument."
  fi
}

parse_args() {
  # shellcheck disable=SC2064
  trap "$(shopt -p nocasematch)" RETURN
  shopt -s nocasematch

  if [[ ! "${1:-}" ]]; then
    fatal_with_usage "${SCRIPT_NAME} requires a command."
  fi

  case "${1}" in
    create_gce_instance_template | create-gce-instance-template)
      CREATE_GCE_INSTANCE_TEMPLATE=1; readonly CREATE_GCE_INSTANCE_TEMPLATE;
      parse_args_create_gce_instance_template "${@}"
      shift 1
      ;;
    prepare_cluster | prepare-cluster)
      PREPARE_CLUSTER=1; readonly PREPARE_CLUSTER;
      parse_args_prepare_cluster "${@}"
      shift 1
      ;;
    -h | --help)
      usage_asm_vm
      exit
      ;;
    --version)
      version_message
      exit
      ;;
    *)
      fatal_with_usage "Unknown command ${1}"
      ;;
  esac
}

parse_args_create_gce_instance_template() {
  # shellcheck disable=SC2064
  trap "$(shopt -p nocasematch)" RETURN
  shopt -s nocasematch

  if [[ ! "${2:-}" || "${2:0:1}" = '-' ]]; then
    fatal_with_usage "Command create_gce_instance_template requires an argument."
  fi
  NEW_INSTANCE_TEMPLATE="${2}"
  shift 2

  while [[ ${#} != 0 ]]; do
    case "${1}" in
      -l | --cluster_location | --cluster-location)
        arg_required "${@}"
        CLUSTER_LOCATION="${2}"
        shift 2
        ;;
      -n | --cluster_name | --cluster-name)
        arg_required "${@}"
        CLUSTER_NAME="${2}"
        shift 2
        ;;
      -p | --project_id | --project-id)
        arg_required "${@}"
        PROJECT_ID="${2}"
        shift 2
        ;;
      -i | --source_instance_template | --source-instance-template)
        arg_required "${@}"
        SOURCE_INSTANCE_TEMPLATE="${2}"
        shift 2
        ;;
      -w | --workload_name | --workload-name)
        arg_required "${@}"
        WORKLOAD_NAME="${2}"
        shift 2
        ;;
      --workload_namespace | --workload-namespace)
        arg_required "${@}"
        WORKLOAD_NAMESPACE="${2}"
        shift 2
        ;;
      -s | --service_account | --service-account)
        arg_required "${@}"
        SERVICE_ACCOUNT="${2}"
        shift 2
        ;;
      -k | --key_file | --key-file)
        arg_required "${@}"
        KEY_FILE="${2}"
        shift 2
        ;;
      --dry_run | --dry-run)
        DRY_RUN=1
        shift 1
        ;;
      --only_validate | --only-validate)
        ONLY_VALIDATE=1
        shift 1
        ;;
      -v | --verbose)
        VERBOSE=1
        shift 1
        ;;
      -h | --help)
        usage_create_gce_instance_template
        exit
        ;;
      *)
        fatal_with_usage "Unknown option ${1}"
        ;;
    esac
  done
}

parse_args_prepare_cluster() {
  # shellcheck disable=SC2064
  trap "$(shopt -p nocasematch)" RETURN
  shopt -s nocasematch

  shift 1
  while [[ ${#} != 0 ]]; do
    case "${1}" in
      -l | --cluster_location | --cluster-location)
        arg_required "${@}"
        CLUSTER_LOCATION="${2}"
        shift 2
        ;;
      -n | --cluster_name | --cluster-name)
        arg_required "${@}"
        CLUSTER_NAME="${2}"
        shift 2
        ;;
      -p | --project_id | --project-id)
        arg_required "${@}"
        PROJECT_ID="${2}"
        shift 2
        ;;
      -s | --service_account | --service-account)
        arg_required "${@}"
        SERVICE_ACCOUNT="${2}"
        shift 2
        ;;
      -k | --key_file | --key-file)
        arg_required "${@}"
        KEY_FILE="${2}"
        shift 2
        ;;
      --dry_run | --dry-run)
        DRY_RUN=1
        shift 1
        ;;
      --only_validate | --only-validate)
        ONLY_VALIDATE=1
        shift 1
        ;;
      -v | --verbose)
        VERBOSE=1
        shift 1
        ;;
      -h | --help)
        usage_prepare_cluster
        exit
        ;;
      *)
        fatal_with_usage "Unknown option ${1}"
        ;;
    esac
  done
}

validate_args() {
  local MISSING_ARGS; MISSING_ARGS=0
  while read -r REQUIRED_ARG; do
    if [[ -z "${!REQUIRED_ARG}" ]]; then
      MISSING_ARGS=1
      warn "Missing value for ${REQUIRED_ARG}"
    fi
    readonly "${REQUIRED_ARG}"
  done <<EOF
CLUSTER_LOCATION
CLUSTER_NAME
PROJECT_ID
EOF

  if [[ "${CREATE_GCE_INSTANCE_TEMPLATE}" -eq 1 ]]; then
    if [[ -z "${WORKLOAD_NAME}" ]]; then
      MISSING_ARGS=1
      warn "Missing value for WORKLOAD_NAME"
    fi
    if [[ -z "${WORKLOAD_NAMESPACE}" ]]; then
      WORKLOAD_NAMESPACE="default"
    fi
  fi

  if [[ "${MISSING_ARGS}" -ne 0 ]]; then
    fatal_with_usage "Missing one or more required options."
  fi

  while read -r FLAG; do
    if [[ "${!FLAG}" -ne 0 && "${!FLAG}" -ne 1 ]]; then
      fatal "${FLAG} must be 0 (off) or 1 (on) if set via environment variables."
    fi
    readonly "${FLAG}"
  done <<EOF
DRY_RUN
ONLY_VALIDATE
VERBOSE
EOF

  if [[ -n "$SERVICE_ACCOUNT" && -z "$KEY_FILE" || -z "$SERVICE_ACCOUNT" && -n "$KEY_FILE" ]]; then
    fatal "Service account and key file must be used together."
  fi

  # since we cd to a tmp directory, we need the absolute path for the key file
  # and yaml file
  if [[ -f "${KEY_FILE}" ]]; then
    KEY_FILE="$(apath -f "${KEY_FILE}")"
    readonly KEY_FILE
  elif [[ -n "${KEY_FILE}" ]]; then
    fatal "Couldn't find key file ${KEY_FILE}."
  fi
}

auth_service_account() {
  info "Authorizing ${SERVICE_ACCOUNT} with ${KEY_FILE}..."
  run gcloud auth activate-service-account \
    --project="${PROJECT_ID}" \
    "${SERVICE_ACCOUNT}" \
    --key-file="${KEY_FILE}"
}

### Environment validation functions ###
validate_dependencies() {
  validate_cli_dependencies
  validate_project
  PROJECT_NUMBER="$(gcloud projects describe "${PROJECT_ID}" \
    --format="value(projectNumber)")"
  GKE_URL=\
"//container.googleapis.com/projects/${PROJECT_ID}/locations/${CLUSTER_LOCATION}/clusters/${CLUSTER_NAME}"
  readonly GKE_URL
  validate_asm_cluster

  if [[ "${CREATE_GCE_INSTANCE_TEMPLATE}" -eq 1 ]]; then
    validate_workloadgroup
    validate_revision_mesh_config
    validate_gce_instance_template
    retrieve_cluster_id
  fi
}

validate_cli_dependencies() {
  local NOTFOUND; NOTFOUND="";
  local EXITCODE; EXITCODE=0;

  info "Checking installation tool dependencies..."
  while read -r dependency; do
    EXITCODE=0
    hash "${dependency}" 2>/dev/null || EXITCODE=$?
    if [[ "${EXITCODE}" -ne 0 ]]; then
      NOTFOUND="${dependency},${NOTFOUND}"
    fi
  done <<EOF
$AGCLOUD
curl
jq
ruby
tr
awk
grep
printf
tail
$AKUBECTL
EOF

  if [[ -n "${NOTFOUND}" ]]; then
    NOTFOUND="$(strip_last_char "${NOTFOUND}")"
    for dep in $(echo "${NOTFOUND}" | tr ' ' '\n'); do
      warn "Dependency not found: ${dep}"
    done
    fatal "One or more dependencies were not found. Please install them and retry."
  fi

  # shellcheck disable=SC2064
  trap "$(shopt -p nocasematch)" RETURN
  shopt -s nocasematch
  if [[ "$(uname -m)" != "x86_64" ]]; then
    fatal "Installation is only supported on x86_64."
  fi
}

validate_project() {
  local RESULT; RESULT=""

  info "Checking for ${PROJECT_ID}..."
  RESULT=$(gcloud projects list \
    --filter="project_id=${PROJECT_ID}" \
    --format="value(project_id)" \
    || true)

  if [[ -z "${RESULT}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF
Unable to find project ${PROJECT_ID}. Please verify the spelling and try
again. To see a list of your projects, run:
  gcloud projects list --format='value(project_id)'
EOF
  fi
}

validate_asm_cluster() {
  validate_cluster
  configure_kubectl
  validate_cluster_registration
  validate_asm_installation
  validate_google_identity_provider
}

validate_cluster() {
  local RESULT; RESULT=""

  info "Confirming cluster information for ${PROJECT_ID}/${CLUSTER_LOCATION}/${CLUSTER_NAME}..."
  RESULT="$(gcloud container clusters list \
    --project="${PROJECT_ID}" \
    --filter="name = ${CLUSTER_NAME} AND location = ${CLUSTER_LOCATION}" \
    --format="value(name)" || true)"
  if [[ -z "${RESULT}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find cluster ${CLUSTER_LOCATION}/${CLUSTER_NAME}.
Please verify the spelling and try again. To see a list of your clusters, in
this project, run:
  gcloud container clusters list --format='value(name,zone)' --project="${PROJECT_ID}"
EOF
  fi
}

validate_asm_installation() {
  info "Checking for istio-system namespace..."
  if [ "$(retry 2 kubectl get ns | grep -c istio-system || true)" -eq 0 ]; then
    fatal "istio-system namespace cannot be found in the cluster. Please install ASM and retry."
  fi

  info "Verifying ASM installation..."
  local SUPPORTED_VERSION_INSTALLED=0
  local EXPANSION_GATEWAY_INSTALLED=0
  local CROSS_CLUSTER_WORKLOADENTRY_ENABLED=0
  local ENVIRON_WORKLOAD_IDENTITY_USED=0
  for deploy in $(kubectl get deployment -n istio-system --no-headers -o custom-columns=":metadata.name"); do
    if [[ "${deploy}" =~ ^$ISTIOD_NAME ]]; then
      local IMAGE_NAME
      IMAGE_NAME="$(retry 2 kubectl get deployment "${deploy}" -n istio-system \
        -o=jsonpath="{.spec.template.spec.containers[0].image}")"
      if [[ "${IMAGE_NAME}" =~ $ASM_RELEASE ]] || \
        [[ -n "${_CI_ASM_IMAGE_TAG}" && "${IMAGE_NAME}" =~ $_CI_ASM_IMAGE_TAG ]]; then
        local CURR_REVISION
        CURR_REVISION="$(retry 2 kubectl get deployment "${deploy}" \
          -n istio-system -ojson | jq -r \
          '.metadata.labels["'"${ASM_REVISION_LABEL_KEY}"'"]')"
        local CURR_VERSION
        CURR_VERSION="$(echo "${IMAGE_NAME}" | sed -n 's/.*:\([0-9]\+.[0-9]\+.[0-9]\+\)-asm.[0-9]/\1/p')"
        if [[ -z "${CURR_VERSION}" ]]; then
          CURR_VERSION="${ASM_FULL_VERSION}"
        fi
        ASM_REVISIONS["${CURR_REVISION}"]="${CURR_VERSION}"
        SUPPORTED_VERSION_INSTALLED=1

        # verify the cross cluster workloadentry registration is enabled.
        local CROSS_CLUSTER_WE_VAL
        CROSS_CLUSTER_WE_VAL="$(retry 2 kubectl get deployment "${deploy}" -n istio-system \
          -ojsonpath='{.spec.template.spec.containers[].env[?(@.name=="'"${CROSS_CLUSTER_WORKLOADENTRY_KEY}"'")].value}')"
        if [[ "${CROSS_CLUSTER_WE_VAL}" == "true" ]]; then
          CROSS_CLUSTER_WORKLOADENTRY_ENABLED=1
        fi

        # verify environ workload identity is being used.
        if uses_environ_workload_identity "${CURR_REVISION}"; then
          ENVIRON_WORKLOAD_IDENTITY_USED=1
        fi
      fi
    fi
    if [[ "${deploy}" =~ ^$EXPANSION_GATEWAY_NAME ]]; then
      EXPANSION_GATEWAY_INSTALLED=1
    fi
  done

  if [[ "${SUPPORTED_VERSION_INSTALLED}" -eq 0 ]]; then
    fatal "ASM ${ASM_RELEASE} version is not found. Please install ASM ${ASM_RELEASE} and retry."
  fi

  if [[ "${CROSS_CLUSTER_WORKLOADENTRY_ENABLED}" -eq 0 ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Multicluster WorkloadEntry registration is not enabled in any of your ASM
${ASM_RELEASE} installations in the cluster. Please install ASM and make sure
none of the values set in your custom overlay override the default values.
EOF
  fi

  if [[ "${ENVIRON_WORKLOAD_IDENTITY_USED}" -eq 0 ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
The environ workload identity is not used in any of your ASM ${ASM_RELEASE}
installation in the cluster. Please install ASM with VM support.
EOF
  fi

  if [[ "${EXPANSION_GATEWAY_INSTALLED}" -eq 0 ]]; then
    if [[ "${CREATE_GCE_INSTANCE_TEMPLATE}" -eq 1 ]] || [[ "${ONLY_VALIDATE}" -eq 1 ]]; then
      { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
${EXPANSION_GATEWAY_NAME} is not found in the cluster.
Please install ASM with VM support or run the current script with the
"prepare_cluster" subcommand.
EOF
    fi
    if [[ "${PREPARE_CLUSTER}" -eq 1 ]]; then
      INSTALL_EXPANSION_GATEWAY=1
    fi
  fi
}

validate_google_identity_provider() {
  info "Verifying identity providers in the cluster..."
  if ! is_google_identity_provider_installed; then
    if [[ "${CREATE_GCE_INSTANCE_TEMPLATE}" -eq 1 ]] || [[ "${ONLY_VALIDATE}" -eq 1 ]]; then
      { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
GCE identity provider is not found in the cluster. Please install ASM with VM
support to allow the script to register the google identity provider in your
cluster or run "prepare_cluster" subcommand of this script.
EOF
    fi
    if [[ "${PREPARE_CLUSTER}" -eq 1 ]]; then
      INSTALL_IDENTITY_PROVIDER=1
    fi
  fi
}

is_google_identity_provider_installed() {
  if ! is_idp_crd_installed; then
    false
    return
  fi

  if ! retry 2 kubectl get identityproviders.security.cloud.google.com -ojsonpath="{..metadata.name}" \
    | grep -w -q google ; then
    false
  fi
}

is_idp_crd_installed() {
  if ! kubectl api-resources --api-group=security.cloud.google.com | grep -q identityproviders; then
    false
  fi
}

validate_cluster_registration() {
  info "Verifying cluster Environ membership..."
  if ! is_cluster_registered; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Cluster is not registered to an environ. Please make sure your cluster is
registered and try again.
EOF
  fi
}

is_cluster_registered() {
  if ! is_membership_crd_installed; then
    false
    return
  fi

  local IDENTITY_PROVIDER
  IDENTITY_PROVIDER="$(retry 2 kubectl get memberships.hub.gke.io \
    membership -ojson 2>/dev/null | jq .spec.identity_provider)"

  if [[ -z "${IDENTITY_PROVIDER}" ]] || [[ "${IDENTITY_PROVIDER}" == 'null' ]]; then
    false
  fi

  populate_environ_info

  local LIST
  LIST="$(gcloud container hub memberships list --project "${ENVIRON_PROJECT_ID}" \
    --format=json | grep "${GKE_URL}")"
  if [[ -z "${LIST}" ]]; then
    false
    return
  fi
}

populate_environ_info() {
  if [[ -n "${ENVIRON_PROJECT_ID}" ]]; then return; fi
  if ! is_membership_crd_installed; then return; fi
  configure_kubectl
  ENVIRON_PROJECT_ID="$(kubectl get memberships.hub.gke.io membership -o=json | jq .spec.workload_identity_pool | sed 's/^\"\(.*\).\(svc\|hub\).id.goog\"$/\1/g')"
}

is_membership_crd_installed() {
  if ! kubectl api-resources --api-group=hub.gke.io | grep -q memberships; then
    false
    return
  fi

  if [[ "$(retry 2 kubectl get memberships.hub.gke.io -ojsonpath="{..metadata.name}" \
    | grep -w -c membership || true)" -eq 0 ]]; then
    false
  fi
}

uses_environ_workload_identity() {
  local CURR_REVISION; CURR_REVISION="${1}";

  local CONFIGMAP; CONFIGMAP="istio-${CURR_REVISION}";
  if [ "${CURR_REVISION}" == "default" ]; then
      CONFIGMAP="istio"
  fi
  if [[ "$(retry 2 kubectl get configmap -n istio-system \
    | grep -w -c "${CONFIGMAP}" || true)" -eq 0 ]]; then
    false
    return
  fi

  if [[ "$(retry 2 kubectl get configmap "${CONFIGMAP}" -n istio-system \
    -ojsonpath='{.data.mesh}' | grep -x -c "^trustDomain: ${PROJECT_ID}.\\(hub\\|svc\\).id.goog$" \
    || true)" -eq 0 ]]; then
    false
  fi
}

validate_workloadgroup() {
  info "Checking for WorkloadGroup ${WORKLOAD_NAME} in namespace ${WORKLOAD_NAMESPACE}..."
  local RESULT; RESULT=""
  RESULT="$(retry 2 kubectl get workloadgroups.networking.istio.io "${WORKLOAD_NAME}" \
    -n "${WORKLOAD_NAMESPACE}" || true)"
  if [[ -z "${RESULT}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find WorkloadGroup ${WORKLOAD_NAME} in namespace ${WORKLOAD_NAMESPACE}.
Please verify the name and namespace of the WorkloadGroup and try again.
EOF
  fi

  parse_workloadgroup
  validate_workload_asm_revision
  validate_workloadgroup_identity_provider
  validate_workload_service_account
}

parse_workloadgroup() {
  retrieve_workload_asm_revision
  retrieve_workload_labels
  retrieve_workload_service_account
}

validate_workloadgroup_identity_provider() {
  info "Verifying the identity provider of the WorkloadGroup..."
  WORKLOAD_IDENTITY_PROVIDER="$(retry 2 kubectl get workloadgroups.networking.istio.io \
    "${WORKLOAD_NAME}" -n "${WORKLOAD_NAMESPACE}" -ojson \
    | jq -r '.spec.metadata.annotations["'"${IDENTITY_PROVIDER_ANNOTATION_KEY}"'"]')"
  if [[ -z "${WORKLOAD_IDENTITY_PROVIDER}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find the identity provider for WorkloadGroup ${WORKLOAD_NAME} in
namespace ${WORKLOAD_NAMESPACE}.
Please make sure an IdentityProvider is specified in the WorkloadGroup.
EOF
  fi

  local RESULT; RESULT=""
  RESULT="$(retry 2 kubectl get identityproviders.security.cloud.google.com \
    "${WORKLOAD_IDENTITY_PROVIDER}" || true)"
  if [[ -z "${RESULT}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find identity provider ${WORKLOAD_IDENTITY_PROVIDER} in the cluster.
To see the available identity providers in the cluster, run:
  kubectl get identityproviders.security.cloud.google.com
EOF
  fi
}

retrieve_workload_asm_revision() {
  info "Retrieving the ASM revision for the WorkloadGroup..."
  WORKLOAD_ASM_REVISION="$(retry 2 kubectl get namespace "${WORKLOAD_NAMESPACE}" -ojson \
    | jq -r '.metadata.labels["'${ASM_REVISION_LABEL_KEY}'"]')"
  if [[ -z "${WORKLOAD_ASM_REVISION}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find an ASM revision for the namespace of the WorkloadGroup. To apply
a revision label on the namespace, replace <REVISION> with an available revision
name and run:
  kubectl label namespace ${WORKLOAD_NAMESPACE} istio-injection- \
  istio.io/rev=<REVISION> --overwrite
EOF
  fi
}

retrieve_workload_labels() {
  info "Retrieving labels from the WorkloadGroup..."
  WORKLOAD_LABELS="$(retry 2 kubectl get workloadgroups.networking.istio.io \
    "${WORKLOAD_NAME}" -n "${WORKLOAD_NAMESPACE}" -ojson | jq -r '.spec.metadata.labels')"
}

retrieve_workload_service_account() {
  info "Retrieving service account from the WorkloadGroup..."
  WORKLOAD_SERVICE_ACCOUNT="$(retry 2 kubectl get workloadgroups.networking.istio.io \
    "${WORKLOAD_NAME}" -n "${WORKLOAD_NAMESPACE}" -ojsonpath="{.spec.template.serviceAccount}")"
  if [[ -z "${WORKLOAD_SERVICE_ACCOUNT}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find the service account for WorkloadGroup ${WORKLOAD_NAME} in
namespace ${WORKLOAD_NAMESPACE}.
Please make sure a service account is specified in the WorkloadGroup.
EOF
  fi
}

validate_workload_asm_revision() {
  info "Verifying the ASM revision for the WorkloadGroup..."

  if [[ -z "${ASM_REVISIONS["${WORKLOAD_ASM_REVISION}"]-}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
ASM revision ${WORKLOAD_ASM_REVISION} for the workload is not found in any ASM
${ASM_RELEASE} installation in the cluster. Please verify the value of
${ASM_REVISION_LABEL_KEY} label on namespace ${WORKLOAD_NAMESPACE} and try again.
EOF
  fi

  if ! uses_environ_workload_identity "${WORKLOAD_ASM_REVISION}"; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
ASM revision ${WORKLOAD_ASM_REVISION} for the workload is not using Environ
workload identity. Please install ASM with VM support.
EOF
  fi
}

validate_workload_service_account() {
  info "Verifying the workload service account..."

  local RESULT; RESULT=""
  RESULT="$(gcloud iam service-accounts list \
    --project="${PROJECT_ID}" \
    --filter="email = ${WORKLOAD_SERVICE_ACCOUNT}" \
    --format="value(name)" || true)"
  if [[ -z "${RESULT}" ]]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find ${WORKLOAD_SERVICE_ACCOUNT} in the project.
Please verify the spelling in the WorkloadGroup and try again.
To see a list of your service accounts, in this project, run:
  gcloud iam service-accounts list --format='value(email)' --project="${PROJECT_ID}"
EOF
  fi
}

validate_revision_mesh_config() {
  info "Verifying the mesh config..."

  if [[ "${WORKLOAD_ASM_REVISION}" == "default" ]]; then
    MESH_CONFIG_NAME="istio"
  else
    MESH_CONFIG_NAME="istio-${WORKLOAD_ASM_REVISION}"
  fi
  readonly MESH_CONFIG_NAME

  if [ "$(retry 2 kubectl get cm -n istio-system --no-headers \
    -o custom-columns=":metadata.name" | grep -c "^${MESH_CONFIG_NAME}$" \
    || true)" -eq 0 ]; then
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find Mesh Config ${MESH_CONFIG_NAME} in your cluster. Please make sure
Anthos Service Mesh is installed successfully and try again.
EOF
  fi
}

validate_gce_instance_template() {
  local RESULT; RESULT=""

  info "Confirming GCE instance template information..."

  if [[ -n "${NEW_INSTANCE_TEMPLATE}" ]]; then
    RESULT="$(gcloud compute instance-templates list \
      --project="${PROJECT_ID}" \
      --filter="name~^${NEW_INSTANCE_TEMPLATE}$" \
      --format="value(name)" || true)"
    if [[ -n "${RESULT}" ]]; then
      { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
GCE instance template ${NEW_INSTANCE_TEMPLATE} already exists.
Please choose a different name and try again. To see a list of your instance templates in
this project, run:
  gcloud compute instance-templates list \
  --format='value(name)' \
  --project="${PROJECT_ID}"
EOF
    fi
  fi

  if [[ -n "${SOURCE_INSTANCE_TEMPLATE}" ]]; then
    RESULT="$(gcloud compute instance-templates list \
      --project="${PROJECT_ID}" \
      --filter="name~^${SOURCE_INSTANCE_TEMPLATE}$" \
      --format="value(name)" || true)"
    if [[ -z "${RESULT}" ]]; then
      { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
Unable to find GCE instance template ${SOURCE_INSTANCE_TEMPLATE}.
Please verify the spelling and try again. To see a list of your instance templates in
this project, run:
  gcloud compute instance-templates list \
  --format='value(name)' \
  --project="${PROJECT_ID}"
EOF
    fi

    info "Verifying the GCE image..."
    SOURCE_IMAGE_PATH="$(retry 2 run gcloud compute instance-templates \
      describe "${SOURCE_INSTANCE_TEMPLATE}" \
      --format=json \
      --project="${PROJECT_ID}" \
      | jq '.properties.disks[0].initializeParams.sourceImage')"

    if [[ "${SOURCE_IMAGE_PATH}" == *projects/*-cloud/global/images* ]]; then
    # check whether the public image is supported
      if check_not_supported_vm_linux_distro ; then
        fatal "Image not supported. Supported image OS distros are: ${SUPPORTED_VM_DISTROS[*]}"
      fi
    else
      info "A custom image found, skip checking"
    fi
    local INSTANCE_TEMPLATE_SERVICE_ACCOUNT
    INSTANCE_TEMPLATE_SERVICE_ACCOUNT="$(gcloud compute instance-templates \
      describe "${SOURCE_INSTANCE_TEMPLATE}" \
      --format="value(properties.serviceAccounts[0].email)" \
      --project="${PROJECT_ID}" || true)"
    if [[ -z "${INSTANCE_TEMPLATE_SERVICE_ACCOUNT}" ]]; then
      { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
The GCE instance template ${SOURCE_INSTANCE_TEMPLATE} does not use a service
account. Please create an instance template with a service account and try again.
EOF
    else
      if [[ "${INSTANCE_TEMPLATE_SERVICE_ACCOUNT}" == "default" ]]; then
        INSTANCE_TEMPLATE_SERVICE_ACCOUNT="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
      fi
      if [[ "${INSTANCE_TEMPLATE_SERVICE_ACCOUNT}" != "${WORKLOAD_SERVICE_ACCOUNT}" ]]; then
        { read -r -d '' MSG; fatal "${MSG}"; } <<EOF || true
The service account used in the instance template ${SOURCE_INSTANCE_TEMPLATE}
does not match the service account used in the WorkloadGroup. Please make sure
they match and try again.
EOF
      fi
    fi
  fi
}

check_not_supported_vm_linux_distro() {
  for distro in "${SUPPORTED_VM_DISTROS[@]}"
  do
    if [[ "${SOURCE_IMAGE_PATH}" = *"${distro}"* ]]; then
      false
      return
    fi
  done
}

set_service_proxy_url() {
  if [[ -n "${SERVICE_PROXY_AGENT_BUCKET}" ]]; then
    return
  fi

  SERVICE_PROXY_AGENT_BUCKET="gs://gce-service-proxy/service-proxy-agent/releases/service-proxy-agent-asm-${ASM_REVISIONS["${WORKLOAD_ASM_REVISION}"]}-stable.tgz"
}

extract_canonical_service_label() {
  local SERVICE_ISTIO_IO_CANONICAL_NAME; SERVICE_ISTIO_IO_CANONICAL_NAME=""
  local APP_KUBERNETES_IO_NAME; APP_KUBERNETES_IO_NAME=""
  local APP; APP=""
  for label in $(echo "${WORKLOAD_LABELS}" | jq -r '. as $k|keys[]|[., $k[.]]|map(tostring+"=")|add'); do
    IFS='=' read -r -a pair <<< "${label}"
    if [[ "${pair[0]}" == "service.istio.io/canonical-name" ]]; then
      SERVICE_ISTIO_IO_CANONICAL_NAME="${pair[1]}"
    fi
    if [[ "${pair[0]}" == "app.kubernetes.io/name" ]]; then
      APP_KUBERNETES_IO_NAME="${pair[1]}"
    fi
    if [[ "${pair[0]}" == "app" ]]; then
      APP="${pair[1]}"
    fi
  done

  if [[ -n "${SERVICE_ISTIO_IO_CANONICAL_NAME}" ]]; then
    CANONICAL_SERVICE="${SERVICE_ISTIO_IO_CANONICAL_NAME}"
  elif [[ -n "${APP_KUBERNETES_IO_NAME}" ]]; then
    CANONICAL_SERVICE="${APP_KUBERNETES_IO_NAME}"
  elif [[ -n "${APP}" ]]; then
    CANONICAL_SERVICE="${APP}"
  else
    CANONICAL_SERVICE="${WORKLOAD_NAME}"
  fi
}

extract_canonical_service_revision() {
  local SERVICE_ISTIO_IO_CANONICAL_REVISION; SERVICE_ISTIO_IO_CANONICAL_REVISION=""
  local APP_KUBERNETES_IO_VERSION; APP_KUBERNETES_IO_VERSION=""
  local VERSION; VERSION=""
  for label in $(echo "${WORKLOAD_LABELS}" | jq -r '. as $k|keys[]|[., $k[.]]|map(tostring+"=")|add'); do
    IFS='=' read -r -a pair <<< "${label}"
    if [[ "${pair[0]}" == "service.istio.io/canonical-revision" ]]; then
      SERVICE_ISTIO_IO_CANONICAL_REVISION="${pair[1]}"
    fi
    if [[ "${pair[0]}" == "app.kubernetes.io/version" ]]; then
      APP_KUBERNETES_IO_VERSION="${pair[1]}"
    fi
    if [[ "${pair[0]}" == "version" ]]; then
      VERSION="${pair[1]}"
    fi
  done

  if [[ -n "${SERVICE_ISTIO_IO_CANONICAL_REVISION}" ]]; then
    CANONICAL_REVISION="${SERVICE_ISTIO_IO_CANONICAL_REVISION}"
  elif [[ -n "${APP_KUBERNETES_IO_VERSION}" ]]; then
    CANONICAL_REVISION="${APP_KUBERNETES_IO_VERSION}"
  elif [[ -n "${VERSION}" ]]; then
    CANONICAL_REVISION="${VERSION}"
  else
    CANONICAL_REVISION="latest"
  fi
}

retrieve_cluster_root_cert() {
  info "Retrieving the cluster's root certificate..."

  local ISTIOD; ISTIOD=""
  ISTIOD="$(retry 2 kubectl get pods -oname -lapp=istiod -n istio-system | awk '{print $1}' | tail -n 1)"
  ROOT_CERT="$(retry 2 run kubectl exec -it -n istio-system "${ISTIOD}" \
    -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    | awk 'NF {sub(/\r/, ""); printf "%s\n",$0;}')"
  if [[ -z "${ROOT_CERT}" ]]; then
    fatal "Error retrieving the cluster's root certificate from Istiod pod ${ISTIOD}. Please try again."
  fi
}

retrieve_cluster_id() {
  info "Retrieving the Istio cluster ID..."

  CLUSTER_ID=$(retry 2 kubectl -n istio-system get cm -l istio.io/rev="${WORKLOAD_ASM_REVISION}" -ojson \
    | jq -r '.items[] | select(.metadata.name | startswith("istio-sidecar-injector")) | .data.values' \
    | jq -r '.global.multiCluster.clusterName')
  if [[ -z "${CLUSTER_ID}" ]]; then
    fatal "Error retrieving the Istio cluster ID. Please try again."
  fi
}

retrieve_expansiongateway() {
  info "Retrieving the cluster's ${EXPANSION_GATEWAY_NAME} IP..."

  INGRESS_IP="$(retry 2 run kubectl get service "${EXPANSION_GATEWAY_NAME}" \
    -n istio-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}')"
  if [[ -z "${INGRESS_IP}" ]]; then
    fatal "Failed to retrieve the ${EXPANSION_GATEWAY_NAME} IP address. Please try again."
  fi
}

retrieve_cluster_network() {
  info "Retrieving the cluster's network name..."

  # TODO possible to share code with scriptaro? or read from something in-cluster to make this more stable.
  NETWORK="$(retry 2 run gcloud container clusters describe "${CLUSTER_NAME}" \
    --zone="${CLUSTER_LOCATION}" \
    --project="${PROJECT_ID}" \
    --format="value(network)")"
}

retrieve_identity_provider() {
  info "Retrieving the identity provider of the workload..."

  populate_environ_info

  local MEMBERSHIPS
  MEMBERSHIPS="$(retry 2 gcloud container hub memberships list --project "${ENVIRON_PROJECT_ID}" \
   --format='value(name)')"
  while read -r MEMBERSHIP; do
    if [[ -n "${MEMBERSHIP}" ]]; then
      local CURR_CLUSTER_URL
      CURR_CLUSTER_URL="$(retry 2 gcloud container hub memberships describe \
        "${MEMBERSHIP}" --project "${ENVIRON_PROJECT_ID}" \
        --format="value(endpoint.gkeCluster.resourceLink)")"
      if [[ "${CURR_CLUSTER_URL}" == "${GKE_URL}" ]]; then
        MEMBERSHIP_UID="$(retry 2 run gcloud container hub memberships describe \
          "${MEMBERSHIP}" --project "${ENVIRON_PROJECT_ID}" --format="value(uniqueId)")"
        break
      fi
    fi
  done <<EOF
${MEMBERSHIPS}
EOF

  CREDENTIAL_IDENTITY_PROVIDER="${MEMBERSHIP_UID}@${WORKLOAD_IDENTITY_PROVIDER}@${WORKLOAD_NAMESPACE}"
}

# A temporary solution to translate yaml to json in shell without relying on yq.
yaml2json () {
  # shellcheck disable=SC2016
  ruby -r yaml -r json -e 'puts YAML.load($stdin.read).to_json'
}

gen_asm_config() {
  local MESH_CONFIG
  MESH_CONFIG=$(retry 2 kubectl get cm "${MESH_CONFIG_NAME}" -n istio-system -ojsonpath='{.data.mesh}' | yaml2json)
  
  local ASM_CONFIG
  ASM_CONFIG=$(echo "${MESH_CONFIG}" | jq -r '.defaultConfig')

  local TRUST_DOMAIN
  TRUST_DOMAIN=$(echo "${MESH_CONFIG}" | jq -r '.trustDomain')

  local ASM_META_REVISION="${WORKLOAD_ASM_REVISION}"
  if [ "${ASM_META_REVISION}" == "default" ]; then
    ASM_META_REVISION=""
  fi

  local LABELS
  LABELS=$(jq -n --arg canonical_service "${CANONICAL_SERVICE}" \
    --arg canonical_revision "${CANONICAL_REVISION}" '{
  "service.istio.io/canonical-name": $canonical_service,
  "service.istio.io/canonical-revision": $canonical_revision
}')

  for label in $(echo "${WORKLOAD_LABELS}" | jq -r '. as $k|keys[]|[., $k[.]]|map(tostring+"=")|add'); do
    IFS='=' read -r -a pair <<< "${label}"
    LABELS=$(jq --arg key "${pair[0]}" \
    --arg value "${pair[1]}" \
    '.[$key]=$value' <<< "${LABELS}")
  done

  local ASM_LABELS_STRING
  ASM_LABELS_STRING=$(jq -sR '' <<< "${LABELS}" | tr -d ' ')

  while read -r -a pair; do
    ASM_CONFIG=$(jq --arg key "${pair[0]:-}" \
    --arg value "${pair[1]:-}" \
    '."proxyMetadata"[$key]=$value' <<< "${ASM_CONFIG}")
  done <<EOF
ISTIO_META_WORKLOAD_NAME $WORKLOAD_NAME
ISTIO_META_MESH_ID proj-$PROJECT_NUMBER
ISTIO_META_NETWORK $PROJECT_ID-$NETWORK
ISTIO_META_ISTIO_VERSION $ASM_META_VERSION
POD_NAMESPACE $WORKLOAD_NAMESPACE
CANONICAL_SERVICE $CANONICAL_SERVICE
CANONICAL_REVISION $CANONICAL_REVISION
USE_TOKEN_FOR_CSR true
ISTIO_META_DNS_CAPTURE true
ISTIO_META_AUTO_REGISTER_GROUP $WORKLOAD_NAME
SERVICE_ACCOUNT $WORKLOAD_SERVICE_ACCOUNT
CREDENTIAL_IDENTITY_PROVIDER $CREDENTIAL_IDENTITY_PROVIDER
TRUST_DOMAIN $TRUST_DOMAIN
ASM_REVISION $ASM_META_REVISION
EOF

  ASM_CONFIG=$(jq --arg key "ISTIO_METAJSON_LABELS" \
    --argjson value "${ASM_LABELS_STRING}" \
    '."proxyMetadata"[$key]=$value' <<< "${ASM_CONFIG}")

  echo "${ASM_CONFIG}"
}

# Generate GCE service proxy definition for the instance template
gen_gce_service_proxy() {
  local ASM_CONFIG
  ASM_CONFIG=$(gen_asm_config)
  
  local PROXY
  PROXY=$(jq -n --arg ingress_ip "${INGRESS_IP}" \
    --arg network "${NETWORK}" \
    --argjson asm_config "${ASM_CONFIG}" '{
  "mode": "ON",
  "proxy-spec": {
    "network": $network,
    "api-server" : ($ingress_ip+":15012"),
    "log-level": "info"
  },
  "asm-config": $asm_config,
  "service": {}
}')

  echo "${PROXY}"
}

# Define the GCE software declaration for OS config.
gce_software_declaration() {
  jq -n --arg ingress_ip "${INGRESS_IP}"\
  --arg asm_revision "${WORKLOAD_ASM_REVISION}" '{
  "softwareRecipes": [{
      "name": "install-gce-service-proxy-agent",
      "desired_state": "INSTALLED",
      "installSteps": [{
          "scriptRun": {
            "script": ("#!/bin/bash
ISTIOD_ENTRY=\""+$ingress_ip+" istiod-"+$asm_revision+".istio-system.svc\"
if ! grep -Fq \"${ISTIOD_ENTRY}\" /etc/hosts; then
  echo \"${ISTIOD_ENTRY} # Added by Anthos Service Mesh\" | sudo tee -a /etc/hosts
fi
DEFAULT_ISTIOD_ENTRY=\""+$ingress_ip+" istiod.istio-system.svc\"
if ! grep -Fq \"${DEFAULT_ISTIOD_ENTRY}\" /etc/hosts; then
  echo \"${DEFAULT_ISTIOD_ENTRY} # Added by Anthos Service Mesh\" | sudo tee -a /etc/hosts
fi
SERVICE_PROXY_AGENT_BUCKET=$(curl \"http://metadata.google.internal/computeMetadata/v1/instance/attributes/gce-service-proxy-agent-bucket\" -H \"Metadata-Flavor: Google\")
ARCHIVE_NAME=$(basename ${SERVICE_PROXY_AGENT_BUCKET})
export SERVICE_PROXY_AGENT_DIRECTORY=$(mktemp -d)
sudo gsutil cp ${SERVICE_PROXY_AGENT_BUCKET} ${SERVICE_PROXY_AGENT_DIRECTORY}
sudo tar -xzf ${SERVICE_PROXY_AGENT_DIRECTORY}/${ARCHIVE_NAME} -C ${SERVICE_PROXY_AGENT_DIRECTORY}
${SERVICE_PROXY_AGENT_DIRECTORY}/service-proxy-agent/service-proxy-agent-bootstrap.sh")
          }
        }
      ]
    }
  ]
}'
}

# Generate metadata for the instance tempalte.
gen_instance_template_metadata() {
  local EXISTING_METADATA; EXISTING_METADATA="${1}";
  jq -n --arg service_proxy_agent_bucket "${SERVICE_PROXY_AGENT_BUCKET}" \
  --arg root_cert "${ROOT_CERT}"\
  --arg declaration "${GCE_SOFTWARE_DECLARATION}"\
  --arg gce_service_proxy "${GCE_SERVICE_PROXY}" '[{
"value": "true",
"key": "enable-guest-attributes"
},
{
  "value": "true",
  "key": "enable-osconfig"
},
{
  "value": $service_proxy_agent_bucket,
  "key": "gce-service-proxy-agent-bucket"
},
{
  "value": $declaration,
  "key": "gce-software-declaration"
},
{
  "value": $root_cert,
  "key": "rootcert"
},
{
  "value": $gce_service_proxy,
  "key": "gce-service-proxy"
}]' | jq ". + [${EXISTING_METADATA}]"
}

# Generate the labels on GCE VMs.
gen_gce_instance_labels() {
  jq -n \
  --arg canonical_service "${CANONICAL_SERVICE}" \
  --arg workload_namespace "${WORKLOAD_NAMESPACE}" \
  --arg project_number "${PROJECT_NUMBER}" '{
"gce-service-proxy": "asm-istiod",
"asm_service_name": $canonical_service,
"asm_service_namespace": $workload_namespace,
"mesh_id": ("proj-"+$project_number)
}'
}

# Generate a default instance template.
gen_default_instance_template() {
  jq -n \
  --arg project_id "${PROJECT_ID}" \
  --arg project_number "${PROJECT_NUMBER}" \
  --arg service_account "${WORKLOAD_SERVICE_ACCOUNT}" \
  --arg instance_template "${NEW_INSTANCE_TEMPLATE}" '{
  "name": $instance_template,
  "properties": {
    "machineType": "n1-standard-1",
    "displayDevice": {
      "enableDisplay": false
    },
    "metadata": {
      "kind": "compute#metadata"
    },
    "disks": [
      {
        "kind": "compute#attachedDisk",
        "type": "PERSISTENT",
        "boot": true,
        "mode": "READ_WRITE",
        "autoDelete": true,
        "deviceName": $instance_template,
        "initializeParams": {
          "sourceImage": "projects/debian-cloud/global/images/family/debian-10",
          "diskType": "pd-standard",
          "diskSizeGb": "10"
        }
      }
    ],
    "canIpForward": false,
    "networkInterfaces": [
      {
        "kind": "compute#networkInterface",
        "network": ("projects/"+$project_id+"/global/networks/default"),
        "accessConfigs": [
          {
            "kind": "compute#accessConfig",
            "name": "External NAT",
            "type": "ONE_TO_ONE_NAT",
            "networkTier": "PREMIUM"
          }
        ]
      }
    ],
    "scheduling": {
      "preemptible": false,
      "onHostMaintenance": "MIGRATE",
      "automaticRestart": true
    },
    "reservationAffinity": {
      "consumeReservationType": "ANY_RESERVATION"
    },
    "serviceAccounts": [
      {
        "email": $service_account,
        "scopes": [
          "https://www.googleapis.com/auth/cloud-platform"
        ]
      }
    ],
    "labels": {}
  }
}'
}

create_gce_instance_template() {
  info "Preparing and creating the GCE instance template..."

  local GCE_SERVICE_PROXY
  GCE_SERVICE_PROXY="$(gen_gce_service_proxy)"

  local GCE_SOFTWARE_DECLARATION
  GCE_SOFTWARE_DECLARATION="$(gce_software_declaration)"

  local VM_LABELS; VM_LABELS="$(gen_gce_instance_labels)"

  # Create the instance template creation request.
  local INSTANCE_TEMPLATE_REQ; INSTANCE_TEMPLATE_REQ="";
  local EXISTING_METADATA; EXISTING_METADATA="";
  if [[ -z "${SOURCE_INSTANCE_TEMPLATE}" ]]; then
    INSTANCE_TEMPLATE_REQ="$(gen_default_instance_template)"
  else
    # Retrieve the existing instance template definition and replace its name
    # with the new instance template name.
    INSTANCE_TEMPLATE_REQ="$(retry 2 run gcloud compute instance-templates \
      describe "${SOURCE_INSTANCE_TEMPLATE}" \
      --format=json \
      --project="${PROJECT_ID}" \
      | jq --arg name "${NEW_INSTANCE_TEMPLATE}" \
      '.name=$name | .properties.disks[0].deviceName=$name' || true)"

    EXISTING_METADATA="$(echo "${INSTANCE_TEMPLATE_REQ}" \
      | jq '.properties.metadata.items')"
  fi

  local METADATA; METADATA="$(gen_instance_template_metadata "${EXISTING_METADATA}")";

  # Inject the GCE VM labels into the instance template.
  INSTANCE_TEMPLATE_REQ=$(jq --argjson metadata "${METADATA}" \
    --argjson labels "${VM_LABELS}" \
   '.properties.metadata.items = $metadata | .properties.labels += $labels' \
   <<< "${INSTANCE_TEMPLATE_REQ}")

  # Create the instance template and get the creation operation.
  local TOKEN; TOKEN="$(retry 2 gcloud \
    --project="${PROJECT_ID}" auth print-access-token)"

  local RESPONSE
  RESPONSE="$(run curl -s -H "Content-Type: application/json" \
    -XPOST "https://www.googleapis.com/compute/v1/projects/${PROJECT_ID}/global/instanceTemplates"\
    -d "${INSTANCE_TEMPLATE_REQ}" \
    -H @- <<EOF
Authorization: Bearer ${TOKEN}
EOF
)"

  unset TOKEN

  local OP_NAME
  OP_NAME="$(echo "${RESPONSE}" | jq '.name' | tr -d '"')"
  if [[ "${OP_NAME}" == "null" ]]; then
    local ERROR;
    ERROR="$(jq '.error.message' <<< "${RESPONSE}")"
    fatal "GCE instance template creation failed: ${ERROR}"
  else
    # Wait and check if the creation is done.
    local RETVAL
    { watch_for_result 5 "^DONE$" gcloud compute operations describe "${OP_NAME}" \
      --format="value(status)" --project="${PROJECT_ID}" >/dev/null; RETVAL="$?"; } || true
    if [[ "${RETVAL}" -ne 0 ]]; then
      { read -r -d '' MSG; fatal "${MSG}"; } <<EOF
Unable to create GCE instance template ${NEW_INSTANCE_TEMPLATE}.
Please check the details of the error:
  gcloud compute operations describe "${OP_NAME}" --project="${PROJECT_ID}"
EOF
    fi
  fi

}

success_message_create_gce_instance_template() {
  info "
*****************************
The GCE Instance Template for ASM VM is now created.

To create a managed instance group from the instance template, run:
  gcloud compute instance-groups managed create <INSTANCE_GROUP_NAME>\
  --template ${NEW_INSTANCE_TEMPLATE}\
  --size <INSTANCE_GROUP_SIZE>\
  --zone <INSTANCE_GROUP_ZONE>\
  --project ${PROJECT_ID}
*****************************
"
}

#######
# set_up_local_workspace does everything that the script needs to avoid
# polluting the environment or current working directory
#######
set_up_local_workspace() {
  info "Setting up necessary files..."
  local OUTPUT_DIR
  info "Creating temp directory..."
  OUTPUT_DIR="$(mktemp -d)"
  info "Created ${OUTPUT_DIR}"
  if [[ -z "${OUTPUT_DIR}" ]]; then
    fatal "Encountered error when running mktemp -d!"
  fi
  pushd "$OUTPUT_DIR" > /dev/null
}

set_kpt_package_url() {
  KPT_URL="https://github.com/GoogleCloudPlatform/anthos-service-mesh-packages"
  KPT_URL="${KPT_URL}.git/asm@${KPT_BRANCH}"
  readonly KPT_URL;
}

needs_kpt() {
  if [[ -z "${AKPT}" ]]; then return; fi
  KPT_VER="$(kpt version)"
  if [[ "${KPT_VER:0:1}" != "0" ]]; then return; fi
  false
}

download_kpt() {
  local PLATFORM
  PLATFORM="$(get_platform "$(uname)" "$(uname -m)")"
  if [[ -z "${PLATFORM}" ]]; then
    fatal "$(uname) $(uname -m) is not a supported platform to perform installation from."
  fi

  local KPT_TGZ
  KPT_TGZ="https://github.com/GoogleContainerTools/kpt/releases/download/v0.39.3/kpt_${PLATFORM}-0.39.3.tar.gz"

  info "Downloading kpt.."
  curl -L "${KPT_TGZ}" | tar xz
  AKPT="$(apath -f kpt)"
}

get_platform() {
  local OS; OS="${1}"
  local ARCH; ARCH="${2}"
  local PLATFORM

  case "${OS}" in
    Linux ) PLATFORM="linux_amd64";;
    Darwin)
      if [[ "${ARCH}" == "arm64" ]]; then
        PLATFORM="darwin_arm64"
      else
        PLATFORM="darwin_amd64"
      fi
    ;;
  esac

  echo "${PLATFORM}"
}

download_asm() {
  local OS

  case "$(uname)" in
    Linux ) OS="linux-amd64";;
    Darwin) OS="osx";;
    *     ) fatal "$(uname) is not a supported OS.";;
  esac

  info "Downloading ASM.."
  local TARBALL; TARBALL="istio-${RELEASE}-${OS}.tar.gz"
  if [[ -z "${_CI_ASM_PKG_LOCATION}" ]]; then
    curl -L "https://storage.googleapis.com/gke-release/asm/${TARBALL}" \
      | tar xz
  else
    local TOKEN; TOKEN="$(retry 2 gcloud --project="${PROJECT_ID}" auth print-access-token)"
    run curl -L "https://storage.googleapis.com/${_CI_ASM_PKG_LOCATION}/asm/${TARBALL}" \
      --header @- <<EOF | tar xz
Authorization: Bearer ${TOKEN}
EOF
    unset TOKEN
  fi

  info "Downloading ASM kpt package..."
  retry 3 kpt pkg get --auto-set=false "${KPT_URL}" asm
}

install_expansion_gateway() {
  
  if [[ -n "${_CI_ASM_IMAGE_LOCATION}" ]]; then
    kpt cfg set asm anthos.servicemesh.hub "${_CI_ASM_IMAGE_LOCATION}"
  fi
  if [[ -n "${_CI_ASM_IMAGE_TAG}" ]]; then
    kpt cfg set asm anthos.servicemesh.tag "${_CI_ASM_IMAGE_TAG}"
  fi

  info "Installing the expansion gateway..."
  retry 5 istioctl install -f "${EXPANSION_GATEWAY_FILE}" \
    --set revision="${REVISION_LABEL}" --skip-confirmation

  # Prevent the stderr buffer from ^ messing up the terminal output below
  sleep 1
  info "...done!"
}

expose_istiod() {
  info "Exposing the control plane for VM workloads..."
  retry 3 kubectl apply -f "${EXPOSE_ISTIOD_SERVICE}"

  for rev in "${!ASM_REVISIONS[@]}"; do
    kpt cfg set asm anthos.servicemesh.istiodHostFQDN "istiod-${rev}.istio-system.svc.cluster.local"
    kpt cfg set asm anthos.servicemesh.istiodHost "istiod-${rev}.istio-system.svc"
    kpt cfg set asm anthos.servicemesh.istiod-vs-name "istiod-vs-${rev}"

    retry 3 kubectl apply -f "${EXPOSE_ISTIOD_SERVICE}"
  done
}

install_google_identity_provider() {
  info "Registering GCE Identity Provider in the cluster..."
  retry 3 kubectl apply -f asm/identity-provider/identityprovider-crd.yaml
  retry 3 kubectl apply -f asm/identity-provider/googleidp.yaml
}

enable_service_mesh_feature() {
  info "Enabling the service mesh feature..."

  local TOKEN
  TOKEN="$(retry 2 gcloud \
    --project="${PROJECT_ID}" auth print-access-token)"

  local RESPONSE
  RESPONSE="$(run curl -s -H "X-Goog-User-Project: ${PROJECT_ID}"  \
    "https://gkehub.googleapis.com/v1alpha1/projects/${PROJECT_ID}/locations/global/features/servicemesh" \
    -H @- <<EOF
Authorization: Bearer ${TOKEN}
EOF
)"

  if [[ "$(echo "${RESPONSE}" | jq -r '.featureState.lifecycleState')" == "ENABLED" ]]; then
    info "The service mesh feature is already enabled."
  elif [[ "$(echo "${RESPONSE}" | jq -r '.error.code')" -eq 404 ]]; then
    run curl -s -H "Content-Type: application/json" \
      -XPOST "https://gkehub.googleapis.com/v1alpha1/projects/${PROJECT_ID}/locations/global/features?feature_id=servicemesh"\
      -d '{servicemesh_feature_spec: {}}' \
      -H @- <<EOF
Authorization: Bearer ${TOKEN}
EOF
  elif [[ "$(echo "${RESPONSE}" | jq -r '.error.code')" -eq 403 ]]; then
    local ERR_MSG
    ERR_MSG="$(echo "${RESPONSE}" | jq -r '.error.message')"
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF
Permission was denied when enabling service mesh feature. Please run install_asm
script to install Anthos Service Mesh to enable all the required permissions.
Original error message was:
${ERR_MSG}
EOF
  else
    { read -r -d '' MSG; fatal "${MSG}"; } <<EOF
Unknown message was returned.
${RESPONSE}
EOF
  fi

  unset TOKEN
}

success_message_prepare_cluster() {
  info "
*****************************
The cluster is ready for adding VM workloads.

Please follow the Anthos Service Mesh for GCE VM user guide to add GCE VMs to
your mesh.
*****************************
"
}

main() {
  init
  parse_args "${@}"
  validate_args

  if [[ -n "${SERVICE_ACCOUNT}" ]]; then
    auth_service_account
  fi

  validate_dependencies
  info "Successfully validated all prerequistes from this shell."
  if [[ "${ONLY_VALIDATE}" -ne 0 ]]; then
    return 0
  fi

  if [[ "${CREATE_GCE_INSTANCE_TEMPLATE}" -eq 1 ]]; then
    set_service_proxy_url
    extract_canonical_service_label
    extract_canonical_service_revision
    retrieve_cluster_root_cert
    retrieve_expansiongateway
    retrieve_cluster_network
    retrieve_identity_provider
    create_gce_instance_template
    success_message_create_gce_instance_template
  fi
  if [[ "${PREPARE_CLUSTER}" -eq 1 ]]; then
    if [[ "${INSTALL_EXPANSION_GATEWAY}" -eq 1 ]] || [[ "${INSTALL_IDENTITY_PROVIDER}" -eq 1 ]]; then
      set_up_local_workspace
      if needs_kpt; then
        download_kpt
      fi
      set_kpt_package_url
      download_asm
      if [[ "${INSTALL_EXPANSION_GATEWAY}" -eq 1 ]]; then
        install_expansion_gateway
        expose_istiod
      fi
      if [[ "${INSTALL_IDENTITY_PROVIDER}" -eq 1 ]]; then
        install_google_identity_provider
      fi
    fi
    enable_service_mesh_feature
    success_message_prepare_cluster
  fi

  return 0
}

main "${@}"
